package com.imooc.course.file.controller.admin;

import com.imooc.course.file.form.FileForm;
import com.imooc.course.server.dto.FileDto;
import com.imooc.course.server.dto.ResponseDto;
import com.imooc.course.server.enums.FileUseEnum;
import com.imooc.course.server.service.FileService;
import com.imooc.course.server.utils.Base64ToMultipartFile;
import com.imooc.course.server.utils.CopyUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.*;

@RestController
@RequestMapping("/admin")
public class UploadController {

    private static final Logger log = LoggerFactory.getLogger(UploadController.class);
    public static final String BUSINESS_NAME = "文件上传";

    @Value("${file.domain}")
    private String FILE_DOMAIN;

    @Value("${file.path}")
    private String FILE_PATH;

    @Resource(name = "fileService")
    private FileService fileService;


    @PostMapping("/upload")
    public ResponseDto upload(@RequestBody FileForm fileForm) throws IOException, InterruptedException {
        //先将fileForm中的shard字符串转换为MultipartFile类
        MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(fileForm.getShard());

        //一些不涉及安全性的数据，尽量将由前端来计算，这样可以减轻服务器压力
        //String fileName = file.getOriginalFilename();
        //String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();

        //通过文件用途参数use将文件保存在不同的目录中，如果没有则创建目录
        FileUseEnum useEnum = FileUseEnum.getByCode(fileForm.getUse());
        String dir = useEnum.name().toLowerCase();
        File fullDir = new File(FILE_PATH + dir);
        if (!fullDir.exists()) {
            fullDir.mkdir();
        }

        //String path = dir + File.separator + fileForm.getKey() + "." + fileForm.getSuffix();
        // 因为String的不可变性，拼接是产生新的对象，如果要拼接的子字符串太多(超过5个)，
        // 频繁的产生新的对象对程序性能不友好，这时应用用StringBuffer，它是线程安全的。
        // 拼接时是直接对StringBuffer操作，不会产生新的对象。但相较于StringBuilder性能速度会慢些。
        String mergeFilePath = new StringBuffer(dir)//合并分片时使用的path
                .append(File.separator)
                .append(fileForm.getKey())
                .append(".")
                .append(fileForm.getSuffix())
                .toString();
        String path = new StringBuffer(mergeFilePath)//保存分片时使用的path
                .append(".")
                .append(fileForm.getShardIndex())
                .toString();

        String fullPath = FILE_PATH + path;
        File dest = new File(fullPath);
        //拼接全路径，通过file.transferTo(File)将文件流写入硬盘中，比getInputStream()方便多了。
        //使用transferTo(dest)后流会关闭，不能再使用shard.getInputStream()方法，否则报错。
        shard.transferTo(dest);
        log.info(dest.getAbsolutePath());

        log.info("开始保存文件记录");
        FileDto fileDto = CopyUtil.copy(fileForm, FileDto.class);
        String fileName = fileForm.getName().replace("." + fileForm.getSuffix(),"");
        fileDto.setPath(mergeFilePath.replace(fileForm.getKey(),fileName));
        fileService.save(fileDto);

        //如果当前分片索引index==分片总数total，说明全部分片已经上传完毕，执行合并分片。
        //基本数据类型使用等号 == ，如果是Integer对象类型则应该使用equals
        if (fileForm.getShardIndex().equals(fileForm.getShardTotal())) {
            //合并完成后的文件以原文件名命名，重设file name。
            fileForm.setName(fileName);
            fileForm.setPath(mergeFilePath);
            this.merge(fileForm);
        }

        //前端需要一个完整的外界访问路径，所以重新设置一下
        fileDto.setPath(FILE_DOMAIN + fileDto.getPath());
        return ResponseDto.success().setContent(fileDto);
    }

    private void merge(FileForm fileForm) throws IOException, InterruptedException {
        log.info("开始：分片合并");
        FileInputStream in = null;
        FileOutputStream out = null ;
        try {
            String filePath = fileForm.getPath().replace(fileForm.getKey(),fileForm.getName());
            File newFile = new File(FILE_PATH + filePath);
            out = new FileOutputStream(newFile, true);

            byte[] bytes = new byte[10*1024*1024];
            int len;
            for (int i = 0; i < fileForm.getShardTotal(); i++) {
                in = new FileInputStream(FILE_PATH + fileForm.getPath() + "." + (i+1));
                while ((len = in.read(bytes)) != -1) {
                    out.write(bytes,0,len);
                }
            }
            log.info("结束：分片合并");
        } catch (FileNotFoundException e) {
            log.error("找不到文件异常",e);
            throw e;
        } catch (IOException e) {
            log.error("IO流异常",e);
            throw e;
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
                log.error("IO流关闭异常",e);
            }
        }

        // 合并完成后，程序在电脑内存并没有完全释放，如果直接删除会删除失败，
        // 即使是手动在电脑硬盘中删除，也会提示文件被占用，无法删除。
        //主动调用System.gc()让jvm执行垃圾回收，并释放资源，这时才能正常删除。
        System.gc();
        //多次调试发现，jvm因为正在运行的线程还没执行完，并不能立刻进行垃圾回收并释放资源。
        //这里强行让线程休眠0.5秒，这个时间已经足够有余，让jvm执行完所有线程并进行垃圾回收。
        Thread.sleep(500);

        log.info("开始：分片删除");
        for (int i = 0; i < fileForm.getShardTotal(); i++) {
            File file = new File(FILE_PATH + fileForm.getPath() + "." + (i+1));
            boolean delete = file.delete();
            String result = delete == true ? "success:" + "删除分片" + (i + 1) : "failed:" + "删除分片" + (i + 1);
            log.info(result);
        }
        log.info("结束：分片删除");
    }

    @GetMapping("/check/{key}")
    public ResponseDto checkShardIndex(@PathVariable("key") String key) {
        FileDto fileDto = fileService.getByShardIndex(key);
        if (fileDto != null) {
            fileDto.setPath(FILE_DOMAIN + fileDto.getPath());
        }
        return ResponseDto.success().setContent(fileDto);
    }
}
